package cz.possoft.client.loader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import cz.possoft.client.utils.PanelHolderUtils;
import cz.possoft.messages.PropertiesManager;

/**
 * This class holds references to ClassLoaders, which loads jars from deploy
 * directory. Classloader is created for each couple of $(key)Generic.jar and
 * $(client)Client.jar.
 * 
 * When jar file is hot-deployed, then new classLoader instance is created and
 * old is discarded. Then references to all active loaded classes is discarded
 * as well (GamePanel is destroyed even if any Game is active).
 * 
 * @author mposolda
 */
public class LoaderHolder {

	// String key should be key prefixes like "Four", "Chess" etc. Single
	// classloader is for couple <Generic, Client>.
	private Map<String, URLClassLoader> classLoaderMap = new ConcurrentHashMap<String, URLClassLoader>(5);
	
	// timestamps for games of given keys. This map is important for decision about redeployment.
	private Map<String, TwoTimestampsHolder> timestampsMap = new HashMap<String, TwoTimestampsHolder>(5);
	private Scanner scanner;

	private static LoaderHolder singleton = null;
	private static Object lock = new Object();
	
	private LoaderHolder() {}
	
	public static LoaderHolder getInstance() throws JarDeploymentException {
		if (singleton == null) {
			synchronized(lock) {
				if (singleton == null) {
					singleton = new LoaderHolder();
					singleton.startScanner();
				}
			}
		}
		return singleton;
	}
	
	/**
	 * Method is called from ClassLoaderObjectInputStream to load classes for loaded games.
	 */
	ClassLoader getClassLoaderByKey(String key) {
		return classLoaderMap.get(key);
	}
	
	/**
	 * Return class for given className with usage of classLoader with given key.
	 */
	public Class<?> getClassForKey(String key, String className) throws ClassNotFoundException {
		ClassLoader cLoader = classLoaderMap.get(key);
		return cLoader.loadClass(className); 
	}
	
	/* PRIVATE METHODS */
	
	/** Scanner starting */
	private void startScanner() {
		scanner = new Scanner();
		new Thread(scanner).start();
	}

	/**
	 * This method should decide, which files (games) should be deployed, which redeployed and which undeployed. After it, 
	 * whole deployment, redeployment and undeployment is performed.
	 * 
	 * The algorithm works this way:
	 * - file should be deployed if it not exists in previous scann (Key is not in classloaderMap)
	 * - file should be undeployed if it exists in previous scann but not now.
	 * - file should be redeployed if it exists in previous scann and also now, but timestamp of any previous file (Generic or Client) is different from current.
	 * 
	 * So main actions performed by this method: 
	 * 
	 * 1) Check which games to deploy or redeploy
	 * 2) Wait some small amount of time (2 seconds). Waiting is performed because we have two jar files for each deployment and we should wait for synchronization
	 * 		   (it is possible that first check was performed during copying of files to deploy dir and only one of files has been copied until the start of interval.
	 * 3) Find keys for undeployment
	 * 4) Find keys for redeployment.
	 * 5) Undeploy all from group 3 and 4.
	 * 6) Deploy all from group 1 (which contains all from group 4). 
	 */	
	void doDeployment(File[] files) throws JarDeploymentException {
	  try {
		// 1
		Collection<String> toDeploy = getKeysForFilesToDeploy(files);
		// 2
		Thread.sleep(DeploymentConfigConstants.DEPLOY_WAIT_SYNC_INTERVAL);
		// 3
		Set<String> toUndeploy = new HashSet<String>(classLoaderMap.size());
		toUndeploy.addAll(classLoaderMap.keySet());
		toUndeploy.removeAll(getAllKeysForFiles(files));
		// 4
		Set<String> toRedeploy = new HashSet<String>(classLoaderMap.size());
		toRedeploy.addAll(classLoaderMap.keySet());
		toRedeploy.retainAll(toDeploy);
		// 5
		toUndeploy.addAll(toRedeploy);
		for (String key : toUndeploy) {
			undeploy(key);
		}
		// 6
		for (String key : toDeploy) {
			File genericJarFile = new File(scanner.getDeployDir() + "/" + key + "Generic.jar");
			File clientJarFile = new File(scanner.getDeployDir() + "/" + key + "Client.jar");
			deploy(key, genericJarFile, clientJarFile);
		}
	  }
	  catch (Exception e) {
		  throw new JarDeploymentException("Exception during deployment", e);
	  }
	}
	
	/*** PRIVATE METHODS ***/
	
	/**
	 * This method should return set of all files, which should be deployed or redeployed.
	 * 
	 */
	private Collection<String> getKeysForFilesToDeploy(File[] files) {
		Set<String> keySetFromMap = classLoaderMap.keySet();
		Set<String> keySetToReturn = new HashSet<String>();
		for (String key : getAllKeysForFiles(files)) {
			if (!keySetFromMap.contains(key)) {
				// should be deployed
				keySetToReturn.add(key);
			}
			else {
				// decide whether should be redeployed based on timestamps.
				File genericJarFile = new File(scanner.getDeployDir() + "/" + key + "Generic.jar");
				File clientJarFile = new File(scanner.getDeployDir() + "/" + key + "Client.jar");
				TwoTimestampsHolder tmNew = new TwoTimestampsHolder(genericJarFile.lastModified(), clientJarFile.lastModified());
				if (!tmNew.equals(timestampsMap.get(key))) {
					keySetToReturn.add(key);
				}
			}
		}
		return Collections.unmodifiableCollection(keySetToReturn);
	}
	
	/**
	 * Get all keys based on files.
	 */
	private Collection<String> getAllKeysForFiles(File[] files) {
		Set<String> keySetToReturn = new HashSet<String>();
		for (File file : files) {
			String key = DeploymentUtils.getPrefixForJarFile(file.getName());
			keySetToReturn.add(key);
		}
		return Collections.unmodifiableCollection(keySetToReturn);
	}
	
	/** Only deploy logic is handled here. So undeploy must be called before! */
	private void deploy(String key, File genericJarFile, File clientJarFile) throws MalformedURLException {
		System.out.println("DEPLOY: Calling deploy for key=" + key + ".");
		URLClassLoader classloader = new URLClassLoader(
				new URL[] { genericJarFile.toURI().toURL(),
						    clientJarFile.toURI().toURL() 
						  }, 
						Thread.currentThread().getContextClassLoader());	
		classLoaderMap.put(key, classloader);
		TwoTimestampsHolder tmNew = new TwoTimestampsHolder(genericJarFile.lastModified(), clientJarFile.lastModified());
		timestampsMap.put(key, tmNew);
		PropertiesManager.getInstance().handleDeployment(key, classloader);
		PanelHolderUtils.handleDeployment(key);
	}
	
	/** Performs undeploying with killing of classLoader and all loaded classes */
	private void undeploy(String key) {
		System.out.println("UNDEPLOY: Calling undeploy for key=" + key + ".");
		classLoaderMap.remove(key);
		timestampsMap.remove(key);
		PanelHolderUtils.handleUndeployment(key);
		PropertiesManager.getInstance().handleUndeployment(key);
	}
	
	/**
	 * Class has information about timestamp of both files.
	 * 
	 * @author mposolda
	 */
	private class TwoTimestampsHolder {
		
		long genericDate;
		long clientDate;
		
		private TwoTimestampsHolder(long genericDate, long clientDate) {
			this.genericDate = genericDate;
			this.clientDate = clientDate;
		}
		
		public boolean equals(Object o) {
			if (o == null) return false;
			if (!(o instanceof TwoTimestampsHolder)) return false;
			TwoTimestampsHolder that = (TwoTimestampsHolder)o;
			return ((that.genericDate == genericDate) && (that.clientDate == clientDate));
		}
		
		public int hashCode() {
			return (int)(genericDate + clientDate);
		}
		
		public String toString() {
			return "genericDate=" + genericDate + ", clientDate=" + clientDate;
		}
	}

}
